package com.shortlink.service.impl;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.Week;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.shortlink.common.exception.ServiceException;
import com.shortlink.mapper.*;
import com.shortlink.mq.producer.ShortLinkStatsSaveProducer;
import com.shortlink.pojos.dto.dataobject.LinkUVTypeDO;
import com.shortlink.pojos.dto.dataobject.PageResult;
import com.shortlink.pojos.dto.dataobject.ShortLinkStatsRecordDO;
import com.shortlink.pojos.dto.req.*;
import com.shortlink.pojos.dto.resp.*;
import com.shortlink.pojos.entity.*;
import com.shortlink.common.properties.LinkProperties;
import com.shortlink.service.ILinkService;
import com.shortlink.utils.*;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.HttpURLConnection;
import java.net.URL;
import java.time.LocalDate;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static com.shortlink.common.constants.AppHttpCodeEnum.LINK_GENERATE_FREQUENTLY;
import static com.shortlink.common.constants.LinkConstant.AMAP_REMOTE_URL;
import static com.shortlink.common.constants.LinkConstant.LINK_NOT_FOUND;
import static com.shortlink.common.constants.RedisConstant.*;

@Service
@RequiredArgsConstructor
public class LinkServiceImpl extends ServiceImpl<LinkMapper, LinkDO> implements ILinkService {

    private final LinkMapper linkMapper;
    private final LinkProperties linkProperties;
    private final RBloomFilter<String> shortUriBloomFilter;
    private final StringRedisTemplate stringRedisTemplate;
    private final RedissonClient redissonClient;
    private final LinkAccessLogsMapper linkAccessLogsMapper;
    private final LinkAccessStatsMapper linkAccessStatsMapper;
    private final LinkBrowserStatsMapper linkBrowserStatsMapper;
    private final LinkDeviceStatsMapper linkDeviceStatsMapper;
    private final LinkLocaleStatsMapper linkLocaleStatsMapper;
    private final LinkNetworkStatsMapper linkNetworkStatsMapper;
    private final LinkOsStatsMapper linkOsStatsMapper;
    private final ShortLinkStatsSaveProducer shortLinkStatsSaveProducer;

    @Value("${shortlink.locale.key}")
    private String localeKey;

    @SneakyThrows
    @Override
    public void jump(String shortUri, ServletRequest request, ServletResponse response) {

        String originUrl = stringRedisTemplate.opsForValue().get(String.format(LINK_JUMP_KEY, shortUri));
        // 若缓存中有，直接跳转
        if(StrUtil.isNotBlank(originUrl)){

            shortLinkStats(buildShortLinkStatsRecordDTO(shortUri,request,response));
            ((HttpServletResponse) response).sendRedirect(originUrl);
            return;
        }

        // 缓存穿透 -> 布隆过滤器
        if (!shortUriBloomFilter.contains(shortUri)) {
            ((HttpServletResponse) response).sendRedirect(LINK_NOT_FOUND);
            return;
        }
        // 缓存穿透 -> 缓存无法跳转链接，处理布隆过滤器误判问题
        String isRedisContain = stringRedisTemplate.opsForValue().get(StrUtil.format(LINK_JUMP_NULL_KEY, shortUri));
        if(StrUtil.isNotBlank(isRedisContain)){
            ((HttpServletResponse) response).sendRedirect(LINK_NOT_FOUND);
            return;
        }

        // 缓存击穿 -> 分布式锁
        RLock lock = redissonClient.getLock(String.format(LOCK_LINK_JUMP_KEY, shortUri));
        lock.lock();
        try {
            // 双重判定锁
            originUrl = stringRedisTemplate.opsForValue().get(String.format(LINK_JUMP_KEY, shortUri));
            if (StrUtil.isNotBlank(originUrl)) {
                ((HttpServletResponse) response).sendRedirect(LINK_NOT_FOUND);
                return;
            }
            // 确认缓存中没有，去数据库查
            Date now=new Date(System.currentTimeMillis());
            LambdaQueryWrapper<LinkDO> wrapper= Wrappers.lambdaQuery(LinkDO.class)
                    .eq(LinkDO::getShortUri,shortUri)
                    .eq(LinkDO::getEnableStatus,"0")
                    .eq(LinkDO::getDelFlag,"0")
                    .and(wq -> wq.eq(LinkDO::getValidDateType,"0")
                            .or(wqOr -> wqOr.lt(LinkDO::getValidDate,now)));
            LinkDO linkDO = linkMapper.selectOne(wrapper);
            if (linkDO!=null&&StrUtil.isNotBlank(linkDO.getOriginUrl())) {
                Date validDate=linkDO.getValidDateType().equals("0")?null:linkDO.getValidDate();
                cachePreheat(shortUri,linkDO.getOriginUrl(),validDate);

                shortLinkStats(buildShortLinkStatsRecordDTO(shortUri,request,response));
                ((HttpServletResponse) response).sendRedirect(linkDO.getOriginUrl());
            }
            else{
                // 查询不到可跳转链接，放入LINK_JUMP_NULL_KEY
                stringRedisTemplate.opsForValue().set(StrUtil.format(LINK_JUMP_NULL_KEY, shortUri),"1");
                ((HttpServletResponse) response).sendRedirect(LINK_NOT_FOUND);
            }
        }
        finally {
            lock.unlock();
        }


    }

    @Override
    public void createLink(LinkCreateReqDTO linkCreateReqDTO) {
        LinkDO linkDO = BeanCopyUtils.copyBean(linkCreateReqDTO, LinkDO.class);
        String uri = generateUri(linkCreateReqDTO.getOriginUrl());
        String url=new StringBuilder()
                .append(linkProperties.getHost())
                .append(":")
                .append(linkProperties.getPort())
                .append("/")
                .append(uri).toString();
        linkDO.setShortUri(uri);
        linkDO.setEnableStatus("0");
        linkDO.setCreateTime(new Date(System.currentTimeMillis()));
        linkDO.setDelFlag("0");
        linkDO.setTotalPV(0);
        linkDO.setTotalUV(0);
        linkDO.setTotalUIP(0);
        linkMapper.insert(linkDO);
        shortUriBloomFilter.add(uri);
        canJump(uri);

        // 缓存预热
        cachePreheat(uri,linkCreateReqDTO.getOriginUrl(),linkCreateReqDTO.getValidDate());
    }

    @Override
    public void updateLink(LinkUpdateReqDTO linkUpdateReqDTO) {
        LinkDO linkDO = BeanCopyUtils.copyBean(linkUpdateReqDTO, LinkDO.class);
        Date now=new Date(System.currentTimeMillis());
        linkDO.setUpdateTime(now);
        LambdaUpdateWrapper<LinkDO> wrapper=Wrappers.lambdaUpdate(LinkDO.class)
                .eq(LinkDO::getShortUri,linkUpdateReqDTO.getShortUri());
        linkMapper.update(linkDO,wrapper);

        if(linkUpdateReqDTO.getEnableStatus()!=null||linkUpdateReqDTO.getValidDateType()=="0"||linkUpdateReqDTO.getValidDate()!=null)
            canJump(linkUpdateReqDTO.getShortUri());
    }

    /**
     * 数据库缓存双删
     * @param shortUri
     */
    @Override
    public void deleteLink(String shortUri) {
        LinkDO linkDO = new LinkDO();
        linkDO.setShortUri(shortUri);
        linkDO.setDelFlag("1");
        Date now=new Date(System.currentTimeMillis());
        linkDO.setDeleteTime(now);
        LambdaUpdateWrapper<LinkDO> wrapper=Wrappers.lambdaUpdate(LinkDO.class)
                .eq(LinkDO::getShortUri,shortUri);
        linkMapper.update(linkDO,wrapper);
        stringRedisTemplate.delete(String.format(LINK_JUMP_KEY, shortUri));
    }

    /**
     * 分页查询短链接
     * @param linkPageQueryReqDTO gid不能为空
     * @return
     */
    @Override
    public PageResult<LinkRespDTO> pageLink(LinkPageQueryReqDTO linkPageQueryReqDTO) {
        Page page = PageUtil.convert(linkPageQueryReqDTO);
        LambdaQueryWrapper<LinkDO> wrapper=Wrappers.lambdaQuery(LinkDO.class)
                .eq(LinkDO::getGid,linkPageQueryReqDTO.getGid())
                .eq(LinkDO::getDelFlag,"0");
        Page res = linkMapper.selectPage(page, wrapper);
        PageResult pageResult = PageUtil.convert(res, LinkRespDTO.class);
        return pageResult;
    }

    @Override
    public PageResult<LinkRespDTO> pageRecycleLink(RecyclePageQueryReqDTO recyclePageQueryReqDTO) {
        Page page = PageUtil.convert(recyclePageQueryReqDTO);
        PageResult<LinkRespDTO> pageResult=new PageResult<>();
        Long total=Long.valueOf(0);
        List<LinkRespDTO> records=new ArrayList<>();
        for (String gid : recyclePageQueryReqDTO.getGids()) {
            // 对每个gid，查询已删除链接
            LambdaQueryWrapper<LinkDO> wrapper=Wrappers.lambdaQuery(LinkDO.class)
                    .eq(LinkDO::getGid,gid)
                    .eq(LinkDO::getDelFlag,"1");
            Page res = linkMapper.selectPage(page, wrapper);
            PageResult tempPageResult = PageUtil.convert(res, LinkRespDTO.class);
            if(tempPageResult==null||tempPageResult.getTotal()==0) continue;
            total+=tempPageResult.getTotal();
            records.add((LinkRespDTO) tempPageResult.getRecords().get(0));
        }
        pageResult.setTotal(total);
        pageResult.setRecords(records);
        return pageResult;
    }

    /**
     * 将已经逻辑删除的链接彻底删除
     * @param shortUri
     */
    @Override
    public void deleteRecycleLink(String shortUri) {
        LambdaUpdateWrapper<LinkDO> wrapper=Wrappers.lambdaUpdate(LinkDO.class)
                .eq(LinkDO::getShortUri,shortUri)
                .eq(LinkDO::getDelFlag,"1")
                .set(LinkDO::getDeleteTime,new Date())
                .set(LinkDO::getDelFlag,"2");
        linkMapper.update(null,wrapper);
    }

    /**
     * 恢复短链接
     * @param shortUri
     */
    @Override
    public void recoverRecycleLink(String shortUri) {
        LambdaQueryWrapper<LinkDO> wrapper=Wrappers.lambdaQuery(LinkDO.class)
                .eq(LinkDO::getShortUri,shortUri);
        LinkDO linkDO=new LinkDO();
        linkDO.setShortUri(shortUri);
        linkDO.setDelFlag("0");
        linkMapper.update(linkDO,wrapper);
        canJump(shortUri);
    }

    @SneakyThrows
    @Override
    public String getDescriptionByUrl(String url) {
        URL targetUrl = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
        connection.setRequestMethod("GET");
        connection.connect();
        int responseCode = connection.getResponseCode();
        if (responseCode == HttpURLConnection.HTTP_OK) {
            Document document = Jsoup.connect(url).get();
            return document.title();
        }
        return "Error while fetching title.";
    }

    @SneakyThrows
    @Override
    public String getFaviconByUrl(String url) {
        URL targetUrl = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
        connection.setRequestMethod("GET");
        connection.connect();
        int responseCode = connection.getResponseCode();
        if (HttpURLConnection.HTTP_OK == responseCode) {
            Document document = Jsoup.connect(url).get();
            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            if (faviconLink != null) {
                return faviconLink.attr("abs:href");
            }
        }
        return null;
    }

    /**
     * 得到监控数据
     * @param requestParam
     * @return
     */
    @Override
    public LinkStatsRespDTO monitoringData(LinkStatsReqDTO requestParam) {
        Date now=DateTimeUtils.getNow();
        Date yesterdayNow=DateTimeUtils.getYesterdayNow();
        Date today=DateTimeUtils.getToday();
        List<Date> weekDates=DateTimeUtils.getWeek();

        if(requestParam.getShortUri()==null)
            return null;
        if(requestParam.getEndDate()==null||requestParam.getStartDate()==null){
            requestParam.setEndDate(now);
            requestParam.setStartDate(yesterdayNow);
        }

        // total pv uv uip
        LinkStatsRespDTO result=new LinkStatsRespDTO();
        LambdaQueryWrapper<LinkDO> linkDOLambdaQueryWrapper=Wrappers.lambdaQuery(LinkDO.class)
                        .eq(LinkDO::getShortUri,requestParam.getShortUri());
        LinkDO linkDO = linkMapper.selectOne(linkDOLambdaQueryWrapper);
        result.setPv(linkDO.getTotalPV());
        result.setUv(linkDO.getTotalUV());
        result.setUip(linkDO.getTotalUIP());

        // 小时访问详情
        // list[0] 0小时 -> hour升序
        LocalDate localDate = LocalDate.now();
        List<LinkAccessStatsDO> todayLinkAccessStatsList = linkAccessStatsMapper.selectList(
                Wrappers.lambdaQuery(LinkAccessStatsDO.class)
                        .eq(LinkAccessStatsDO::getShortUri, requestParam.getShortUri())
                        .eq(LinkAccessStatsDO::getDate, today)
        );
        int[] hours=new int[24];
        todayLinkAccessStatsList.forEach(each ->{
            Integer hour = each.getHour();
            hours[hour]=each.getPv();
        });
        List<Integer> hourStats= IntStream.of(hours).boxed().collect(Collectors.toList());
        result.setHourStats(hourStats);

        // 高频访问IP详情 前十
        // list[0] 第一名 -> 出现次数降序
        List<LinkStatsTopIpRespDTO> topIpStats=linkAccessLogsMapper.linkAccessTop10(requestParam);
        result.setTopIpStats(topIpStats);

        // 一周访问情况
        // list[0] -> 周一
//        List<Integer> weekdayStats=weekDates.stream()
//                        .map(each -> {
//                            LinkAccessStatsDO respDTO = linkAccessStatsMapper.selectOne(
//                                    Wrappers.lambdaQuery(LinkAccessStatsDO.class)
//                                            .eq(LinkAccessStatsDO::getDate, each)
//                            );
//                            return respDTO==null?0:respDTO.getPv();
//                        }).collect(Collectors.toList());
        int[] weeks=new int[7];
        todayLinkAccessStatsList.forEach(each -> {
            Integer weekday=each.getWeekday();
            weeks[weekday-1]=each.getPv();
        });
        List<Integer> weekdayStats=IntStream.of(weeks).boxed().collect(Collectors.toList());
        result.setWeekdayStats(weekdayStats);

        // 每天基础访问情况
        List<LinkAccessStatsDO> dailyLinkAccessStatsList = linkAccessStatsMapper.selectList(
                Wrappers.lambdaQuery(LinkAccessStatsDO.class)
                        .eq(LinkAccessStatsDO::getShortUri, requestParam.getShortUri())
                        .le(LinkAccessStatsDO::getDate, requestParam.getEndDate())
                        .ge(LinkAccessStatsDO::getDate, requestParam.getStartDate())
        );
        List<LinkStatsAccessDailyRespDTO> daily = dailyLinkAccessStatsList.stream().map(each -> {
            LinkStatsAccessDailyRespDTO respDTO = BeanCopyUtils.copyBean(each, LinkStatsAccessDailyRespDTO.class);
            return respDTO;
        }).collect(Collectors.toList());
        result.setDaily(daily);

        // 地区访问详情（仅国内）
        List<LinkLocaleStatsDO> linkLocaleStatsList=linkLocaleStatsMapper.selectList(
                Wrappers.lambdaQuery(LinkLocaleStatsDO.class)
                        .eq(LinkLocaleStatsDO::getShortUri, requestParam.getShortUri())
                        .le(LinkLocaleStatsDO::getDate, requestParam.getEndDate())
                        .ge(LinkLocaleStatsDO::getDate, requestParam.getStartDate())
        );
        int localeCntSum = linkLocaleStatsList.stream().mapToInt(LinkLocaleStatsDO::getCnt).sum();
        List<LinkStatsLocaleRespDTO> localeCnStats = linkLocaleStatsList.stream()
                .map(each -> {
                    LinkStatsLocaleRespDTO respDTO = BeanCopyUtils.copyBean(each, LinkStatsLocaleRespDTO.class);
                    respDTO.setRatio(getRadioByCS(each.getCnt(),localeCntSum));
                    return respDTO;
                }).collect(Collectors.toList());
        result.setLocaleCnStats(localeCnStats);

        // 浏览器访问情况
        List<LinkBrowserStatsDO> linkBrowserStatsDOList = linkBrowserStatsMapper.selectList(
                Wrappers.lambdaQuery(LinkBrowserStatsDO.class)
                        .eq(LinkBrowserStatsDO::getShortUri, requestParam.getShortUri())
                        .le(LinkBrowserStatsDO::getDate, requestParam.getEndDate())
                        .ge(LinkBrowserStatsDO::getDate, requestParam.getStartDate())
        );
        int browserCntSum = linkBrowserStatsDOList.stream().mapToInt(LinkBrowserStatsDO::getCnt).sum();
        List<LinkStatsBrowserRespDTO> browserStats=linkBrowserStatsDOList.stream()
                        .map(each -> {
                            LinkStatsBrowserRespDTO respDTO = BeanCopyUtils.copyBean(each, LinkStatsBrowserRespDTO.class);
                            respDTO.setRatio(getRadioByCS(each.getCnt(),browserCntSum));
                            return respDTO;
                        }).collect(Collectors.toList());
        result.setBrowserStats(browserStats);

        // 操作系统访问情况
        List<LinkOsStatsDO> linkOsStatsDOList = linkOsStatsMapper.selectList(
                Wrappers.lambdaQuery(LinkOsStatsDO.class)
                        .eq(LinkOsStatsDO::getShortUri, requestParam.getShortUri())
                        .le(LinkOsStatsDO::getDate, requestParam.getEndDate())
                        .ge(LinkOsStatsDO::getDate, requestParam.getStartDate())
        );
        int osCntSum = linkOsStatsDOList.stream().mapToInt(LinkOsStatsDO::getCnt).sum();
        List<LinkStatsOsRespDTO> osStats=linkOsStatsDOList.stream()
                        .map(each -> {
                            LinkStatsOsRespDTO respDTO=BeanCopyUtils.copyBean(each,LinkStatsOsRespDTO.class);
                            respDTO.setRatio(getRadioByCS(each.getCnt(),osCntSum));
                            return respDTO;
                        }).collect(Collectors.toList());
        result.setOsStats(osStats);

        // 访客访问类型情况
        List<LinkStatsUvTypeRespDTO> uvTypeStats=new ArrayList<>();
        LinkUVTypeDO uvType = linkAccessLogsMapper.linkUVType(requestParam);
        if(uvType!=null){
            Integer oldUserCnt=uvType.getOldUserCnt();
            Integer newUserCnt=uvType.getNewUserCnt();
            Double oldRadio=getRadioByCS(oldUserCnt,oldUserCnt+newUserCnt);
            Double newRadio=getRadioByCS(newUserCnt,oldUserCnt+newUserCnt);
            uvTypeStats.add(new LinkStatsUvTypeRespDTO(oldUserCnt,"0",oldRadio));
            uvTypeStats.add(new LinkStatsUvTypeRespDTO(newUserCnt,"1",newRadio));
        }
        result.setUvTypeStats(uvTypeStats);

        // 访问设备类型情况
        List<LinkDeviceStatsDO> linkDeviceStatsDOList = linkDeviceStatsMapper.selectList(
                Wrappers.lambdaQuery(LinkDeviceStatsDO.class)
                        .eq(LinkDeviceStatsDO::getShortUri, requestParam.getShortUri())
                        .le(LinkDeviceStatsDO::getDate, requestParam.getEndDate())
                        .ge(LinkDeviceStatsDO::getDate, requestParam.getStartDate())
        );
        int deviceCntSum=linkDeviceStatsDOList.stream().mapToInt(LinkDeviceStatsDO::getCnt).sum();
        List<LinkStatsDeviceRespDTO> deviceStats=linkDeviceStatsDOList.stream()
                        .map(each -> {
                            LinkStatsDeviceRespDTO respDTO = BeanCopyUtils.copyBean(each, LinkStatsDeviceRespDTO.class);
                            respDTO.setRatio(getRadioByCS(each.getCnt(),deviceCntSum));
                            return respDTO;
                        }).collect(Collectors.toList());
        result.setDeviceStats(deviceStats);

        // 访问网络类型情况
        List<LinkNetworkStatsDO> linkNetworkStatsDOList = linkNetworkStatsMapper.selectList(
                Wrappers.lambdaQuery(LinkNetworkStatsDO.class)
                        .eq(LinkNetworkStatsDO::getShortUri, requestParam.getShortUri())
                        .le(LinkNetworkStatsDO::getDate, requestParam.getEndDate())
                        .ge(LinkNetworkStatsDO::getDate, requestParam.getStartDate())
        );
        int networkCntSum = linkNetworkStatsDOList.stream().mapToInt(LinkNetworkStatsDO::getCnt).sum();
        List<LinkStatsNetworkRespDTO> networkStats=linkNetworkStatsDOList.stream()
                        .map(each -> {
                            LinkStatsNetworkRespDTO respDTO = BeanCopyUtils.copyBean(each, LinkStatsNetworkRespDTO.class);
                            respDTO.setRatio(getRadioByCS(each.getCnt(),networkCntSum));
                            return respDTO;
                        }).collect(Collectors.toList());
        result.setNetworkStats(networkStats);

        return result;
    }

    private Double getRadioByCS(int cnt, int sum){
        double ratio = (double) cnt / sum;
        return Math.round(ratio * 100.0) / 100.0;
    }


    /**
     * 记录跳转
     * @param shortLinkStatsRecord 短链接统计实体参数
     */
    @Override
    public void shortLinkStats(ShortLinkStatsRecordDO shortLinkStatsRecord) {
        Map<String, String> producerMap = new HashMap<>();
        producerMap.put("statsRecord", JSON.toJSONString(shortLinkStatsRecord));
        shortLinkStatsSaveProducer.send(producerMap);
    }

    /**
     * 记录跳转，实际执行
     * linkAccessLogs linkAccessStats LinkBrowserStats LinkDeviceStats LinkLocaleStats LinkNetworkStats LinkOsStats
     * @param requestParam
     */
    @Transactional
    public void actualSaveShortLinkStats(ShortLinkStatsRecordDO requestParam){

        Date currentDate = requestParam.getCurrentDate();
        int hour = DateUtil.hour(currentDate, true);
        Week week = DateUtil.dayOfWeekEnum(currentDate);
        int weekValue = week.getValue()-1;
        Map<String, Object> localeParamMap = new HashMap<>();
        localeParamMap.put("key", localeKey);
        localeParamMap.put("ip", requestParam.getIp());
        String localeResultStr = HttpUtil.get(AMAP_REMOTE_URL, localeParamMap);
        JSONObject localeResultObj = JSON.parseObject(localeResultStr);
        String infoCode = localeResultObj.getString("infocode");
        String actualProvince = "未知";
        if (StrUtil.isNotBlank(infoCode) && StrUtil.equals(infoCode, "10000")){
            String province = localeResultObj.getString("province");
            actualProvince=StrUtil.equals(province, "[]")? actualProvince:province;
        }

        LinkAccessLogsDO linkAccessLogsDO = BeanCopyUtils.copyBean(requestParam, LinkAccessLogsDO.class);
        linkAccessLogsDO.setProvince(actualProvince);
        linkAccessLogsMapper.insert(linkAccessLogsDO);

        LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
                .shortUri(requestParam.getShortUri())
                .date(requestParam.getToday())
                .pv(1)
                .uv(requestParam.getUvFirstFlag()?1:0)
                .uip(requestParam.getUipFirstFlag()?1:0)
                .hour(hour)
                .weekday(weekValue)
                .build();
        linkAccessStatsMapper.shortLinkAccessStats(linkAccessStatsDO);

        LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                .shortUri(requestParam.getShortUri())
                .date(requestParam.getToday())
                .cnt(1)
                .browser(requestParam.getBrowser())
                .build();
        linkBrowserStatsMapper.shortLinkBrowserStats(linkBrowserStatsDO);

        LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                .shortUri(requestParam.getShortUri())
                .date(requestParam.getToday())
                .cnt(1)
                .device(requestParam.getDevice())
                .build();
        linkDeviceStatsMapper.shortLinkDeviceStats(linkDeviceStatsDO);

        LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                .shortUri(requestParam.getShortUri())
                .date(requestParam.getToday())
                .cnt(1)
                .province(actualProvince)
                .build();
        linkLocaleStatsMapper.shortLinkLocaleStats(linkLocaleStatsDO);

        LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                .shortUri(requestParam.getShortUri())
                .date(requestParam.getToday())
                .cnt(1)
                .network(requestParam.getNetwork())
                .build();
        linkNetworkStatsMapper.shortLinkNetworkStats(linkNetworkStatsDO);

        LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                .shortUri(requestParam.getShortUri())
                .date(requestParam.getToday())
                .cnt(1)
                .os(requestParam.getOs())
                .build();
        linkOsStatsMapper.shortLinkOsStats(linkOsStatsDO);

        linkMapper.incrementStats(requestParam.getShortUri(), 1, requestParam.getUvFirstFlag() ? 1 : 0, requestParam.getUipFirstFlag() ? 1 : 0);

    }

    /**
     * 根据请求构建ShortLinkStatsRecordDTO
     * 返回的uv要求为uuid，可能非法，但影响不大
     * @param shortUri
     * @param request
     * @param response
     * @return
     */
    private ShortLinkStatsRecordDO buildShortLinkStatsRecordDTO(String shortUri, ServletRequest request, ServletResponse response){

        AtomicBoolean uvFirstFlag = new AtomicBoolean();// 是否uv
        Cookie[] cookies = ((HttpServletRequest) request).getCookies();
        AtomicReference<String> uv = new AtomicReference<>();

        /**
         * cookie为空时调用，为cookie赋值
         * runnable内变量需为原子
         */
        Runnable addResponseCookieTask = () -> {
            uv.set(UUID.fastUUID().toString());
            // 创建cookie对象，区分不同用户
            Cookie uvCookie = new Cookie("uv", uv.get());
            // cookie最大存活时间 30天
            uvCookie.setMaxAge(60 * 60 * 24 * 30);
            // todo 更改路由时更改 检验cookie是否成功
            uvCookie.setPath("/jump/"+shortUri);
            ((HttpServletResponse) response).addCookie(uvCookie);
            uvFirstFlag.set(Boolean.TRUE);
            stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UV_KEY + shortUri, uv.get());
        };

        if (ArrayUtil.isNotEmpty(cookies)) {
            Arrays.stream(cookies)
                    .filter(each -> Objects.equals(each.getName(), "uv"))
                    .findFirst()
                    .map(each -> {
                        if (each != null && each.getValue() != null) {
                            String value = each.getValue();
                            uv.set(value);
                            Long uvAdded = stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UV_KEY + shortUri, value);
                            uvFirstFlag.set(uvAdded != null && uvAdded > 0L);
                            return value;
                        } else {
                            addResponseCookieTask.run();
                            return null; // 或者返回一个默认值，或者抛出一个异常，取决于你的业务逻辑
                        }
                    });
        } else {
            addResponseCookieTask.run();
        }

        String remoteAddr = LinkUtil.getActualIp(((HttpServletRequest) request));
        String os = LinkUtil.getOs(((HttpServletRequest) request));
        String browser = LinkUtil.getBrowser(((HttpServletRequest) request));
        String device = LinkUtil.getDevice(((HttpServletRequest) request));
        String network = LinkUtil.getNetwork(((HttpServletRequest) request));
        Long uipAdded = stringRedisTemplate.opsForSet().add(SHORT_LINK_STATS_UIP_KEY + shortUri, remoteAddr);
        boolean uipFirstFlag = uipAdded != null && uipAdded > 0L;
        return ShortLinkStatsRecordDO.builder()
                .shortUri(shortUri)
                .user(uv.get())
                .uvFirstFlag(uvFirstFlag.get())
                .uipFirstFlag(uipFirstFlag)
                .ip(remoteAddr)
                .os(os)
                .browser(browser)
                .device(device)
                .network(network)
                .currentDate(DateTimeUtils.getNow())
                .today(DateTimeUtils.getToday())
                .build();
    }

    /**
     * 创建唯一uri
     * @param originUrl
     * @return
     */
    private String generateUri(String originUrl){
        int customGenerateCount = 0;
        String shorUri;
        while (true) {
            if (customGenerateCount > 10) {
                throw new ServiceException(LINK_GENERATE_FREQUENTLY);
            }
            originUrl += UUID.randomUUID().toString();
            shorUri = HashUtil.hashToBase62(originUrl);
            if (!shortUriBloomFilter.contains(shorUri)) {
                break;
            }
            customGenerateCount++;
        }
        return shorUri;
    }

    /**
     * 该方法删除缓存中的不可用标识
     *
     * 当链接从“不可跳转”变为"可跳转",如进行恢复、创建、重置有效期、启用 等操作时
     * 抽离出方法以降低耦合度
     * @param uri
     */
    private void canJump(String uri){
        String isRedisContain = stringRedisTemplate.opsForValue().get(StrUtil.format(LINK_JUMP_NULL_KEY, uri));
        if(StrUtil.isNotBlank(isRedisContain)){
            stringRedisTemplate.delete(StrUtil.format(LINK_JUMP_NULL_KEY, uri));
        }
    }

    /**
     * 缓存预热，将url放入缓存
     * @param uri
     * @param originUrl
     * @param validDate 过期日期，永久有效则传null
     */
    private void cachePreheat(String uri,String originUrl,Date validDate){
        stringRedisTemplate.opsForValue().set(
                String.format(LINK_JUMP_KEY,uri),
                originUrl,
                LinkUtil.getLinkCacheValidTime(validDate),
                TimeUnit.MILLISECONDS
        );
    }


}
